import json
import sys
import threading
import wave
from os import path, system

import pyaudio
from PyQt6.QtCore import Qt, QFile
from PyQt6.QtGui import QIcon, QPixmap
from PyQt6.QtNetwork import QHostAddress
from PyQt6.QtWidgets import QDialog, QApplication, QTableWidgetItem, QGraphicsPixmapItem, QGraphicsTextItem, \
    QFileDialog, QMessageBox

import GlobalData
import WebService
from FileServer import FileServer
from SQLConn import SQLConn
from ui.WeChat_ui import Ui_Dialog
from widget.WeChatGraphicsView import WeChatGraphicsView


class WeChat(QDialog, Ui_Dialog):
    def __init__(self):
        super(WeChat, self).__init__()
        self.setupUi(self)
        self.setWindowIcon(QIcon('image/wechat.jpg'))
        self.setWindowFlag(Qt.WindowType.MSWindowsFixedSizeDialogHint)
        # 初始化网络和数据库
        self.files = FileServer()
        WebService.init()
        WebService.udpsocket.readyRead.connect(self.recvUdpData)
        WebService.tcpfileserver.newConnection.connect(self.files.handleFileSend)
        WebService.tcpfilesocket.readyRead.connect(self.files.handleFileRecv)
        self.sql = SQLConn()
        # 登录用户
        WebService.setOnline(True)
        # 正在聊天的用户
        self.peer = None
        self.initUi()
        # 文件
        self.fileid = 0
        self.filesend: QFile | None = None
        self.payloadsize = 64 * 1024

    def initUi(self):
        # 背景
        self.lbSidebar.setPixmap(QPixmap('image/侧边栏.jpg'))
        self.lbProfilePhoto.setPixmap(QPixmap('data/photo/%s.jpg' % GlobalData.currentUser))
        self.lbSearchbar.setPixmap(QPixmap('image/搜索栏.jpg'))
        self.lbLayer.setPixmap(QPixmap('image/默认图层.jpg'))
        self.lbToolbar.setPixmap(QPixmap('image/工具栏.jpg'))
        # 好友列表
        self.tbwFriendList.setColumnWidth(0, self.tbwFriendList.width())
        self.tbwFriendList.itemSelectionChanged.connect(self.loadChatLog)
        # 聊天信息
        self.gvWeChatView = WeChatGraphicsView(-1, 62, 625, 417, self.page_2)
        self.y0 = -(self.gvWeChatView.height() / 2) + 40
        self.pbSend.clicked.connect(self.sendMessage)
        self.pbTransFile.clicked.connect(self.sendFile)
        self.pbVoiceEnable.setIcon(QIcon('image/voiceoff.jpg'))
        self.pbVoiceEnable.clicked.connect(self.enableVoice)
        self.pbSendVoice.setIcon(QIcon('image/pressedspeak.jpg'))
        self.pbSendVoice.setVisible(False)
        self.pbSendVoice.pressed.connect(self.startSpeak)
        self.pbSendVoice.released.connect(lambda: setattr(self, 'recoding', False))
        self.gvWeChatView.mouseDoubleClicked.connect(self.openFile)

    def loadChatLog(self):
        """
        加载聊天信息
        """
        self.stackedWidget.setCurrentIndex(1)
        item: QTableWidgetItem = self.tbwFriendList.currentItem()
        self.peer = item.data(Qt.ItemDataRole.UserRole)
        for chat in self.sql.loadChats(self.peer):
            selfSend = chat[1] == GlobalData.currentUser
            self.showMessage(chat[0], chat[3], selfSend)

    def recvUdpData(self):
        """
        接收来自 Udp 服务器的数据
        """
        socket = WebService.udpsocket
        while socket.hasPendingDatagrams():
            size = socket.pendingDatagramSize()
            data, host, port = socket.readDatagram(size)
            data = json.loads(data.decode('utf-8'))
            dtype = data['Type']
            if dtype == 'Online':
                # 加载好友列表
                self.tbwFriendList.clear()
                for index, name in enumerate(data['Body']):
                    item = QTableWidgetItem(QIcon('data/photo/%s.jpg' % name), name)
                    item.setData(Qt.ItemDataRole.UserRole, name)
                    item.setData(Qt.ItemDataRole.UserRole + 1, index + 1)
                    self.tbwFriendList.insertRow(index)
                    self.tbwFriendList.setRowHeight(index, 60)
                    self.tbwFriendList.setItem(index, 0, item)
            elif dtype == 'Message' or dtype == 'File':
                # 接收聊天 / 文件信息
                sender = data['Username']
                receiver = data['Peername']
                body = data['Body']
                date = data['Datetime']
                self.sql.insertChat(dtype, sender, receiver, body, date)
                # 显示聊天信息
                if sender == self.peer:
                    self.showMessage(dtype, body, True)
                elif receiver == self.peer:
                    self.showMessage(dtype, body, False)

    def showMessage(self, dtype, body, selfSend):
        """
        展示信息
        :param dtype: 消息类型
        :param body: 信息内容
        :param selfSend: 是否是自己发送的
        """
        scene = self.gvWeChatView.scene()
        # 消息尺寸
        if dtype == 'Message':
            w = len(body) * 16
            h = 32
        else:
            w = 100
            h = 30
        # 消息位置
        if selfSend:
            x = self.gvWeChatView.width() / 2 - w - 5 - 32 - 30
        else:
            x = -self.gvWeChatView.width() / 2 + 30 + 32 + 5
        y = self.y0
        # 边框
        if selfSend:
            texture = QPixmap('image/TextureSelf.jpg')
        else:
            texture = QPixmap('image/TexturePeer.jpg')
        texture = texture.scaled(w, h, Qt.AspectRatioMode.IgnoreAspectRatio)
        texture = QGraphicsPixmapItem(texture)
        texture.setPos(x, y)
        scene.addItem(texture)
        self.y0 += h + 40
        # 消息内容
        if dtype == 'Message':
            text = QGraphicsTextItem(body)
            font = text.font()
            font.setPointSize(10)
            text.setFont(font)
            text.setPos(x + 5, y + 5)
            scene.addItem(text)
        # 头像
        if selfSend:
            photo = QPixmap('data/photo/%s.jpg' % GlobalData.currentUser)
            px = x + w + 5
        else:
            photo = QPixmap('data/photo/%s.jpg' % self.peer)
            px = x - 32 - 5
        photo = photo.scaled(32, 32, Qt.AspectRatioMode.KeepAspectRatio)
        photo = QGraphicsPixmapItem(photo)
        photo.setPos(px, y)
        scene.addItem(photo)

    def sendMessage(self):
        """
        发送文本消息
        """
        message = self.teChatEdit.toPlainText()
        if len(message) > 0:
            WebService.sendTextMessage(self.peer, message)
            self.showMessage('Message', message, True)
            self.teChatEdit.clear()

    def enableVoice(self):
        """
        发送语音消息（状态切换）
        """
        if self.pbSendVoice.isVisible():
            self.pbSendVoice.setVisible(False)
            self.pbVoiceEnable.setIcon(QIcon('image/voiceoff.jpg'))
        else:
            self.pbSendVoice.setVisible(True)
            self.pbVoiceEnable.setIcon(QIcon('image/voiceon.jpg'))

    def startSpeak(self):
        """
        开始录音
        """
        self.frames = []
        self.rpa = pyaudio.PyAudio()
        self.stream = self.rpa.open(format=self.rpa.get_format_from_width(width=2),  # 采样格式
                                    channels=2,                                      # 声道数
                                    rate=44100,                                      # 采样率
                                    input=True,                                      # 录音模式
                                    frames_per_buffer=1024)
        self.recoding = True
        threading._start_new_thread(self.onSpeaking, ())

    def onSpeaking(self):
        while self.recoding:
            # 录音帧
            self.frames.append(self.stream.read(1024))
        # 录音停止
        self.stream.stop_stream()
        self.stream.close()
        self.rpa.terminate()
        # 保存
        file = 'data/files/voice/voice.wav'
        f = wave.open(file, 'wb')
        f.setnchannels(2)
        f.setsampwidth(2)
        f.setframerate(44100)
        f.writeframes(b''.join(self.frames))
        f.close()
        # 发送文件
        self.sendFile(file)

    def sendFile(self, file=None):
        """
        发送文件
        :param file: 文件，None 则弹窗选择
        """
        if file is None:
            file = QFileDialog.getOpenFileName(self)[0]
        if file != '':
            filepath = path.abspath(file)
            if path.exists(filepath):
                file = QFile(filepath)
                if WebService.tcpfileserver.listen(QHostAddress.SpecialAddress.Any, WebService.tport):
                    self.filesend = file
                    self.showMessage('File', self.filesend, True)
                    WebService.beginSendFile(file.fileName(), self.peer, file.size())
                else:
                    QMessageBox.critical(self, '错误', '网络连接失败')
            else:
                QMessageBox.critical(self, '错误', '文件 %s 不存在' % filepath)

    def handleSendFile(self):
        file = self.filesend
        socket = WebService.tcpfileserver.nextPendingConnection()
        # 打开文件
        file.open(QFile.OpenModeFlag.ReadOnly)

        def write():
            """
            持续发送数据
            """
            while not file.atEnd():
                socket.write(file.read(self.payloadsize))
            # 发送完成 断开连接
            socket.abort()
            WebService.tcpfileserver.close()

        socket.bytesWritten.connect(write)
        # 发送数据
        block = file.read(self.payloadsize)
        socket.write(block)
        file.close()

    def openFile(self, pos):
        """
        双击下载 / 打开文件
        :param pos: 鼠标位置
        """
        pos = self.gvWeChatView.mapToScene(pos)
        item = self.gvWeChatView.scene().itemAt(pos, self.gvWeChatView.transform())
        if item is not None:
            filepath = item.data(Qt.ItemDataRole.UserRole)
            if filepath is None:
                return
            if not path.exists(filepath):
                # 下载文件
                filename = path.basename(filepath)
                filepath = path.abspath('data/files/%s' % filename)
                item.setData(Qt.ItemDataRole.UserRole, filepath)
                sender = item.data(Qt.ItemDataRole.UserRole + 1)
                WebService.downloadFile(filename, sender)
            else:
                # 展示文件
                system('explorer.exe %s' % filepath)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = WeChat()
    window.show()
    code = app.exec()
    WebService.setOnline(False)
    sys.exit(code)
